/* 
 *  Copyright 2012 CodeMagi, Inc.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.codemagi.login.model;

import com.codemagi.util.JCrypt;
import com.codemagi.util.UIDGenerator;
import com.codemagi.util.Utils;
import java.io.Serializable;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * Standard implementation of the IUser interface.
 *
 *  @version 1.0
 *  @author August Detlefsen for CodeMagi, Inc.
 */
public class User extends com.codemagi.servlets.model.BaseBean implements IUser, Serializable {

    //MEMBERS
    protected String username;
    private transient String password;
    private transient String salt;
    protected Date dateCreated   = new Date();
    protected Date dateModified  = new Date();
    protected Date dateExpires;
    protected Date lastLogin     = new Date(1l);
    protected Date previousLogin = new Date(1l);
    protected Boolean online         = new Boolean(true);
    protected Boolean updateRequired = new Boolean(false);
    protected Boolean passwordUpdateRequired = new Boolean(false);
    protected String prefix;
    protected String firstName;
    protected String middleName;
    protected String lastName;
    protected String suffix;
    protected String company;
    protected String title;
    protected String email;
    protected String phone;
    protected String extension;
    protected String fax;
    protected String mobile;
    protected String address1;
    protected String address2;
    protected String address3;
    protected String city;
    protected String state;
    protected String zipCode;
    protected String country;

    protected String ssoHash;
    private Integer failedLogins    = new Integer(0);
    private Date lockedUntil;

    protected String recoveryHash;
    protected Date recoveryExpiration;

    protected Integer creatorId;

    protected Vector groups     = new Vector();   //permission groups this user belongs to

    //is this user authenticated? 
    private boolean authenticated = false;
    

    //CONSTANTS
    private static final Group SU = new Group();
    static {
	SU.setGroupName("SUPERUSER");
    }


    //GETTERS AND SETTERS

    /**
     * Returns the user ID as a String
     *
     * @deprecated
     */
    public String getUserId() {
	if (getId() == null) return "";

	return getId().toString();
    }


    //AUTHENTICATION METHODS
    /**
     * Authenticates a user based on password ONLY. Checks for 
     * online true/false or user expired should be handled programatically.
     */
    public boolean authenticate(String inPassword) {

        if (Utils.isEmpty(inPassword)) return false;

	//first attempt to authenticate via SHA-256
	String hashPassword = hashPassword( inPassword );

	authenticated = hashPassword.equals(password);

	if (!authenticated) {
	    //attempt to authenticate user with old-style JCrypt algorithm
	    try {
		hashPassword = JCrypt.crypt(JCrypt.getSalt(password), inPassword);
	    
		if ( hashPassword.equals(password) ) {
		    setNewPassword(inPassword);    //sets authenticated = true and updates password to SHA-256
		}

	    } catch (Exception e) {
		// AIOOBE in JCrypt for example
	    }
	}

        return authenticated;
    }

    /**
     * Returns true if this user has been authenticated, false otherwise.
     */
    public boolean isAuthenticated() {
        return authenticated && online.booleanValue();
    }

    /**
     * Sets a new password (in plain text) into the bean. Encrypts
     * incoming plaintext, sets password, sets authenticated to true.
     */
    public void setNewPassword(String newValue) {
	
	if ( Utils.isEmpty(newValue) ) return;

	password = hashPassword( newValue );

        authenticated = true;
    }

    /**
     * Determines if a user is in a particular application group. Always returns true if
     * user is superuser. Returns false if the User is not authenticated.
     *
     * @param group      The name of the group to check for
     * @return boolean   True if the user is a member of the group, false otherwise
     */
    public boolean isInGroup(String group) {
	Group test = new Group();
	test.setGroupName(group);

	return (isAuthenticated() && (groups.contains(SU) || groups.contains(test)) );
    }


    /**
     * Determines if a user is in a particular application group. Always returns true if
     * user is superuser. Returns false if the User is not authenticated.
     *
     * @param group      The ID of the group to check for
     * @return boolean   True if the user is a member of the group, false otherwise
     */
    public boolean isInGroup(Integer groupId) {

	if (groupId == null || !isAuthenticated()) return false;

	if (isSuperuser()) return true;

	Iterator i = groups.iterator();
	while (i.hasNext()) {
	    Group g = (Group)i.next();

	    if ( groupId.equals(g.getId()) ) return true;
	}

	return false;
    }

    /**
     * Determines if a user is in not in particular application group.
     * Returns true if the User is not authenticated.
     *
     * @param group      The name of the group to check for
     * @return boolean   False if the user is a member of the group, true otherwise
     */
    public boolean isNotInGroup(String group) {
        Group test = new Group();
        test.setGroupName(group);

        if (!isAuthenticated()) return true;

        return !(groups.contains(test));
    }

    /**
     * Determines if a user is NOT in a particular application group. Returns true if the User is not authenticated.
     *
     * @param group      The ID of the group to check for
     * @return boolean   False if the user is a member of the group, true otherwise
     */
    public boolean isNotInGroup(Integer groupId) {

        if (groupId == null || !isAuthenticated()) return true;

        Iterator i = groups.iterator();
        while (i.hasNext()) {
            Group g = (Group)i.next();

            if ( groupId.equals(g.getId()) ) return false;
        }

        return true;
    }

    /**
     * returns true if this user is a superuser
     */
    public boolean isSuperuser() {
	return isAuthenticated() && groups.contains(SU);
    }


    //GETTERS AND SETTERS
    public void setUsername(String newValue) {
        username = newValue;
    }

    public String getUsername() {
        return username;
    }

    public void setPassword(String newValue) {
        password = newValue;
    }

    public String getPassword() {
        return password;
    }

    public void setSalt(String newValue) {
        salt = newValue;
    }

    public String getSalt() {
        return salt;
    }

    public void setDateCreated(Date newValue) {
        dateCreated = newValue;
    }

    public Date getDateCreated() {
        return dateCreated;
    }

    public void setDateModified(Date newValue) {
        dateModified = newValue;
    }

    public Date getDateModified() {
        return dateModified;
    }

    public void setDateExpires(Date newValue) {
        dateExpires = newValue;
    }

    public Date getDateExpires() {
        return dateExpires;
    }

    public void setLastLogin(Date newValue) {
        if (newValue == null) newValue = new Date(1l);

	setPreviousLogin(lastLogin);

        lastLogin = newValue;
    }

    public Date getLastLogin() {
        return lastLogin;
    }

    /**
     * we track the previous login for some apps
     * for example to highlight new things for the user
     */
    public void setPreviousLogin(Date newValue) {
	previousLogin = newValue;
    }

    public Date getPreviousLogin() {
	return previousLogin;
    }

    public void setOnline(Boolean newValue) {
        if (newValue != null) online = newValue;
    }

    public Boolean getOnline() {
        return online;
    }

    public void setUpdateRequired(Boolean newValue) {
        if (newValue != null) updateRequired = newValue;
    }

    public Boolean getUpdateRequired() {
        return updateRequired;
    }

    public void setPasswordUpdateRequired(Boolean newValue) {
        if (newValue != null) passwordUpdateRequired = newValue;
    }

    public Boolean getPasswordUpdateRequired() {
        return passwordUpdateRequired;
    }

    public void setPrefix(String newValue) {
        prefix = newValue;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setFirstName(String newValue) {
        firstName = newValue;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setMiddleName(String newValue) {
        middleName = newValue;
    }

    public String getMiddleName() {
        return middleName;
    }

    public void setLastName(String newValue) {
        lastName = newValue;
    }

    public String getLastName() {
        return lastName;
    }

    public void setSuffix(String newValue) {
        suffix = newValue;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setCompany(String newValue) {
        company = newValue;
    }

    public String getCompany() {
        return company;
    }

    public void setTitle(String newValue) {
        title = newValue;
    }

    public String getTitle() {
        return title;
    }

    public void setEmail(String newValue) {
        email = newValue;
    }

    public String getEmail() {
        return email;
    }

    public void setPhone(String newValue) {
        phone = newValue;
    }

    public String getPhone() {
        return phone;
    }

    public void setExtension(String newValue) {
        extension = newValue;
    }

    public String getExtension() {
        return extension;
    }

    public void setFax(String newValue) {
        fax = newValue;
    }

    public String getFax() {
        return fax;
    }

    public void setMobile(String newValue) {
        mobile = newValue;
    }

    public String getMobile() {
        return mobile;
    }

    public void setAddress1(String newValue) {
        address1 = newValue;
    }

    public String getAddress1() {
        return address1;
    }

    public void setAddress2(String newValue) {
        address2 = newValue;
    }

    public String getAddress2() {
        return address2;
    }

    public void setAddress3(String newValue) {
        address3 = newValue;
    }

    public String getAddress3() {
        return address3;
    }

    public void setCity(String newValue) {
        city = newValue;
    }

    public String getCity() {
        return city;
    }

    public void setState(String newValue) {
        state = newValue;
    }

    public String getState() {
        return state;
    }

    public void setZipCode(String newValue) {
        zipCode = newValue;
    }

    public String getZipCode() {
        return zipCode;
    }

    public void setCountry(String newValue) {
        country = newValue;
    }

    public String getCountry() {
        return country;
    }

    public void setSsoHash(String newValue) {
        ssoHash = newValue;
    }

    public String getSsoHash() {
        return ssoHash;
    }

    public void setFailedLogins(Integer newValue) {
        if (newValue != null) failedLogins = newValue;
    }

    public Integer getFailedLogins() {
        return failedLogins;
    }

    public void setLockedUntil(Date newValue) {
        lockedUntil = newValue;
    }

    public Date getLockedUntil() {
        return lockedUntil;
    }

    public void setGroups(Vector newValue) {
	if (newValue != null) groups = newValue;
    }

    public Vector getGroups() {
	return groups;
    }

    public void setCreatorId(Integer newValue) {
	creatorId = newValue;
    }

    public Integer getCreatorId() {
	return creatorId;
    }

    public void setRecoveryHash(String newValue) {
        recoveryHash = newValue;
    }

    public String getRecoveryHash() {
        return recoveryHash;
    }

    public void setRecoveryExpiration(Date newValue) {
        recoveryExpiration = newValue;
    }

    public Date getRecoveryExpiration() {
        return recoveryExpiration;
    }


    // SPECIAL GETTERS AND SETTERS -------------------

    public void incrementFailedLogins() {
        failedLogins++;
    }

    /** 
     * Returns the User's full name: First Middle Last
     */
    public String getFullName() {
	StringBuffer output = new StringBuffer(32);

	if (!Utils.isEmpty(firstName)) output.append(firstName).append(" ");
	if (!Utils.isEmpty(middleName)) output.append(middleName).append(" ");
	if (!Utils.isEmpty(lastName)) output.append(lastName);

	return output.toString();
    }


    /**
     * Alias for getFullName()
     */
    public String getName() {
	return getFullName();
    }


    /**
     * Returns the User's full name: Last, First Middle
     */
    public String getFullNameLastFirst() {
	StringBuffer output = new StringBuffer(32);
	
	output.append(lastName).append(", ").append(firstName);
	if (!Utils.isEmpty(middleName)) output.append(" ").append(middleName);

	return output.toString();
    }

    /**
     * Adds a Group to this User
     */
    public void addGroup(Group newValue) {
	if ( !groups.contains(newValue) ) {
	    groups.add(newValue);
	}
    }

    /** 
     * Removes this User from a Group
     */
    public void removeGroup(Group newValue) {
	groups.remove(newValue);
    }

    /**
     * returns an ArrayList of Group IDs (Integer) that this user is part of
     */
    public List getGroupIds() {
	ArrayList output = new ArrayList(groups.size());
	Iterator i = groups.iterator();
	while (i.hasNext()) {
	    Group g = (Group)i.next();
	    output.add(g.getId());
	}
	return output;
    }

    public boolean isUpdateRequired() {
	return updateRequired.booleanValue();
    }

    public boolean isPasswordUpdateRequired() {
	return passwordUpdateRequired.booleanValue();
    }


    //UTILITY METHODS ------------------------------------------------------------------

    private String hashPassword(String inPassword) {

	if (Utils.isEmpty(salt)) salt = UIDGenerator.getUID();

	try {
	    Mac mac = Mac.getInstance("HmacSHA256");
	    mac.init( new SecretKeySpec(salt.getBytes(), "HmacSHA256") );

	    byte[] hashed  = mac.doFinal( inPassword.getBytes() );
	    String encoded = com.Ostermiller.util.Base64.encodeToString(hashed);

	    return encoded;

	} catch (java.security.GeneralSecurityException gse) {
	    //no-op
	}

	return null;
    }

}
